package cn.xutongxue.interview.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.xutongxue.interview.entity.Question;
import cn.xutongxue.interview.entity.QuestionBank;
import cn.xutongxue.interview.entity.QuestionBankMap;
import cn.xutongxue.interview.entity.User;
import cn.xutongxue.interview.entity.dto.questionBankMap.QuestionBankMapRemoveBatchDto;
import cn.xutongxue.interview.enums.Errors;
import cn.xutongxue.interview.exception.BusinessException;
import cn.xutongxue.interview.mapper.QuestionBankMapMapper;
import cn.xutongxue.interview.mapper.QuestionBankMapper;
import cn.xutongxue.interview.service.QuestionBankMapService;
import cn.xutongxue.interview.service.QuestionService;
import cn.xutongxue.interview.service.UserService;
import cn.xutongxue.interview.utils.ThrowUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author Administrator
 * @description 针对表【question_bank_map(题库题目关联表)】的数据库操作Service实现
 * @createDate 2024-09-11 16:43:44
 */
@Slf4j
@Service
public class QuestionBankMapServiceImpl extends ServiceImpl<QuestionBankMapMapper, QuestionBankMap>
        implements QuestionBankMapService {


    @Resource
    @Lazy
    private QuestionService questionService;

    @Resource
    private QuestionBankMapper questionBankMapper;

    @Resource
    @Lazy
    private UserService userService;

    @Resource
    @Lazy
    private QuestionBankMapService questionBankMapService;

    @Override
    public void addQuestionToBankBatch(Long bankId, List<Long> questionIdList) {
        Object loginUserId = StpUtil.getLoginIdDefaultNull();
        User currentUser = userService.getById((String) loginUserId);
        // 参数校验
        ThrowUtils.throwIf(bankId < 0, Errors.PARAMS_ERROR, "题库ID不能为空");
        ThrowUtils.throwIf(CollUtil.isEmpty(questionIdList), Errors.PARAMS_ERROR, "题目列表不能为空");
        ThrowUtils.throwIf(loginUserId == null, Errors.NOT_LOGIN_ERROR);


        // 数据库中存在的题目id
        LambdaQueryWrapper<Question> queryWrapper = Wrappers.lambdaQuery(Question.class)
                .select(Question::getId)
                .in(Question::getId, questionIdList);
        // 只查询题目id出来优化性能
        List<Long> effectiveQuestionIdList = questionService.listObjs(queryWrapper, obj -> (Long) obj);
        ThrowUtils.throwIf(CollUtil.isEmpty(effectiveQuestionIdList), Errors.PARAMS_ERROR, "合法题目为空");


        //  检查题目是否存在题库中，避免重复插入
        LambdaQueryWrapper<QuestionBankMap> lambdaQueryWrapper = Wrappers.lambdaQuery(QuestionBankMap.class)
                .eq(QuestionBankMap::getBankId, bankId)
                .in(QuestionBankMap::getQuestionId, effectiveQuestionIdList);
        List<QuestionBankMap> existQuestionList = this.list(lambdaQueryWrapper);

        // 题库中已存在的题目ID
        Set<Long> existQuestionIdSet = existQuestionList.stream()
                .map(QuestionBankMap::getQuestionId)
                .collect(Collectors.toSet());
        ThrowUtils.throwIf(CollUtil.isEmpty(effectiveQuestionIdList), Errors.PARAMS_ERROR, "所有题库已存在该题库中");

        // 过滤掉已存在的题目id 得到待添加的题目id
        effectiveQuestionIdList = effectiveQuestionIdList.stream()
                .filter(questionId -> !existQuestionIdSet.contains(questionId))
                .collect(Collectors.toList());

        // 判断题库是否存在
        QuestionBank questionBank = questionBankMapper.selectById(bankId);
        ThrowUtils.throwIf(questionBank == null, Errors.PARAMS_ERROR, "题库不存在");

        // 批量操作数据入库 属于IO密集型，可以吧线程数设置大一点，为核心数量的 2 - 4 倍（大概）
        // 自定义线程池 - 核心线程数20， 最大线程数50， 线程空闲时间60S，最大阻塞数1000，拒绝策略： 由调用线程处理任务
        ThreadPoolExecutor customExecutor = new ThreadPoolExecutor(
                20,
                50,
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        // 保存所有批次任务
        List<CompletableFuture<Void>> futures = new ArrayList<>();


        // 执行批量插入操作 - 分批处理，避免长事务
        int batchSize = 1000;
        int totalQuestionListSize = effectiveQuestionIdList.size();
        for (int i = 0; i < totalQuestionListSize; i += batchSize) {
            // 生成每一批次需要插入的数据
            List<Long> subQuestionIdList = effectiveQuestionIdList.subList(i, Math.min(i + batchSize, totalQuestionListSize));
            List<QuestionBankMap> questionBankMapList = subQuestionIdList.stream().map(questionId -> {
                QuestionBankMap questionBankMap = new QuestionBankMap();
                questionBankMap.setBankId(bankId);
                questionBankMap.setQuestionId(questionId);
                questionBankMap.setUserId(currentUser.getId());
                return questionBankMap;
            }).collect(Collectors.toList());


            // 异步处理每批数据，将数据添加到异步任务列表
            // todo：可以优化增加异常处理，防止任务中断
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                // 使用事务每批数据
                // 获取代理 - 使用this.对象会导致事务失效，因为this.调用的是类本身，而不是代理对象
                // 方法1 使用AOP获取代理
                // QuestionBankMapService questionBankMapService = (QuestionBankMapServiceImpl) AopContext.currentProxy();
                // questionBankMapService.addQuestionToBankBatchInner(questionBankMapList);
                // 方法2 注入服务类，直接用Spring Boot 框架注入依赖
                questionBankMapService.addQuestionToBankBatchInner(questionBankMapList);
            }, customExecutor);
            futures.add(future);
        }
        // 等待所有批次任务执行完毕
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        // 执行结束 关闭线程池
        customExecutor.shutdown();
    }

    /**
     * 批量添加题目到题库（事务，仅供内部调用）
     *
     * @param questionBankMapList 待插入的关系集合
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addQuestionToBankBatchInner(List<QuestionBankMap> questionBankMapList) {
        try {
            boolean result = this.saveBatch(questionBankMapList);
            ThrowUtils.throwIf(!result, Errors.OPERATION_ERROR, "批量插入题目到题库失败");
        } catch (DataIntegrityViolationException e) {
            log.error("数据库唯一键冲突违反其他完整性约束:  错误信息:{}", e.getMessage());
            throw new BusinessException(Errors.OPERATION_ERROR, "题库中已存在该题目，无法重复添加");
        } catch (DataAccessException e) {
            log.error("数据库连接问题、事务问题等导致操作失败: 错误信息:{}", e.getMessage());
            throw new BusinessException(Errors.OPERATION_ERROR, "数据库操作失败");
        } catch (Exception e) {
            log.error("批量插入题库发生未知错误: 错误信息:{}", e.getMessage());
            throw new BusinessException(Errors.OPERATION_ERROR, "批量插入题目到题库失败");
        }

    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteQuestionBankMapBatch(QuestionBankMapRemoveBatchDto removeBatchDto) {
        Long bankId = removeBatchDto.getBankId();
        List<Long> questionIdList = removeBatchDto.getQuestionIdList();

        // 参数校验
        ThrowUtils.throwIf(bankId < 0, Errors.PARAMS_ERROR, "题库ID不能为空");
        ThrowUtils.throwIf(CollUtil.isEmpty(questionIdList), Errors.PARAMS_ERROR, "题目ID列表不能为空");

        // 查询需要删除的题目映射， 使用in语法 消除循环，减少数据库IO
        LambdaQueryWrapper<QuestionBankMap> wrapper = Wrappers.lambdaQuery(QuestionBankMap.class)
                .eq(QuestionBankMap::getBankId, bankId)
                .in(QuestionBankMap::getQuestionId, questionIdList);

        // 批量删除
        boolean result = this.remove(wrapper);
        ThrowUtils.throwIf(!result, Errors.OPERATION_ERROR, "批量删除题目题库关联失败");
    }

}




